﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.EventHandling;
using ScrumTable.DL.Data.Common;
using ScrumTable.DL.Data.Generic.Cache;
using ScrumTable.DL.Data.Generic.Properties;
using ScrumTable.DL.Data.Schema;
using ScrumTable.DL.Data.Schema.ScrumTableV1;

#endregion

namespace ScrumTable.DL.Data.Generic
{
    /// <summary>
    /// This class specifies the base data object. Each data object may contain
    /// multiple values which can be retrieved with the indexer. A data object
    /// has a unique identifier and an order index, which is given by the
    /// underlying system.
    /// The implemented properties are specified in their corresponding
    /// DataObject Schema descriptions.
    /// </summary>
    /// <typeparam name="TSchema">Specifies the data schema stored in the current data object.</typeparam>
    /// <typeparam name="TElement">Specifies the type of the underlying element.</typeparam>
    /// <remarks>The implementation of the data object class follows the Anything pattern.</remarks>
    internal abstract class DataObjectBase<TSchema, TElement> : DisposeBase, IDataObject
        where TSchema : SchemaBase
        where TElement : CacheElementBase
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets a list of schema name to property mappings.
        /// </summary>
        protected readonly SchemaMapper NameSchemaMappings = new SchemaMapper();

        /// <summary>
        /// Gets a list of cache element property name to DAO property name mappings.
        /// </summary>
        private readonly IDictionary<string, string> NamePropertyMappings = new Dictionary<string, string>
                                                                                {
                                                                                    /* { [cache property], [DAO property] } */
                                                                                    { "BackEndId", "BackEndId"},
                                                                                    { "IsSynchronized", "IsSynchronized"},
                                                                                };

        private readonly DataAccessObjectBase _parent;
        private readonly FastSmartWeakEvent<PropertyChangedEventHandler> _pptyChangedEvents = new FastSmartWeakEvent<PropertyChangedEventHandler>();
        private readonly FastSmartWeakEvent<PropertyChangingEventHandler> _pptyChangingEvents = new FastSmartWeakEvent<PropertyChangingEventHandler>();

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets the ID of the current data object.
        /// </summary>
        public string Id { get { return Element.UniqueId; } }

        /// <summary>
        /// Gets the unique id of the underlying back end system identity.
        /// </summary>
        public string BackEndId { get { return Element.BackEndId; } }

        /// <summary>
        /// Gets true if the changes have been synchronized with the back-end system.
        /// </summary>
        public bool IsSynchronized { get { return Element.IsSynchronized; } }
        
        /// <summary>
        /// Gets the underlying system order index. This property can be used
        /// to output the elements in the order given by the data connector.
        /// </summary>
        public virtual double OrderIndex { get { return Element.OrderIndex; } }

        /// <summary>
        /// Gets the corresponding data schema (e.g. UserStorySchema / TaskSchema / ...).
        /// </summary>
        public SchemaBase Schema { get { return TypedSchema; } }

        /// <summary>
        /// Gets or sets a value of the current data object. The data format
        /// must be specified in a separate document in order to support
        /// later updates and avoid broken interfaces.
        /// </summary>
        /// <param name="name">Name of the value to access.</param>
        /// <returns>Returns the retrieved data object.</returns>
        public virtual object this[string name]
        {
            get
            {
                SchemaBase schemaField = Schema[name];

                if (!NameSchemaMappings.ContainsSchema(schemaField))
                    throw new ArgumentException(string.Format(Resources.ExcDataObjArgNotInSchema, name), "name");

                if (NameSchemaMappings.GetGetMethod(schemaField) == null)
                    throw new InvalidOperationException(Resources.ExcDataObjArgGetNotSupported);

                return NameSchemaMappings.GetGetMethod(schemaField)(schemaField);
            }
            set
            {
                SchemaBase schemaField = Schema[name];

                if (!NameSchemaMappings.ContainsSchema(schemaField))
                    throw new ArgumentException(string.Format(Resources.ExcDataObjArgTypeInvalid, name, schemaField.SchemaType.FullName), "name");

                if (NameSchemaMappings.GetSetMethod(schemaField) == null)
                    throw new InvalidOperationException(Resources.ExcDataObjArgSetNotSupported);

                CheckIsReadOnly(schemaField);
                CheckValueType(schemaField, value);
                CheckAllowedValues(schemaField, value);
                CheckMaxLength(schemaField, value);

                NameSchemaMappings.GetSetMethod(schemaField)(schemaField, value);
            }
        }

        /// <summary>
        /// Gets the underlying scrum talbe schema.
        /// </summary>
        protected ScrumTableV1Schema ScrumTableSchema { get { return _parent.Context.Schema; } }

        /// <summary>
        /// Gets the corresponding typed data schema (e.g. UserStorySchema / TaskSchema / ...).
        /// </summary>
        protected abstract TSchema TypedSchema { get; }

        /// <summary>
        /// Gets the underlying data element instance.
        /// </summary>
        protected abstract TElement Element { get; }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DataObject instance, no public constructor
        /// needed.
        /// </summary>
        /// <param name="parent">Specifies the parent data access object.</param>
        internal DataObjectBase(DataAccessObjectBase parent)
        {
            PreCondition.AssertNotNull(parent, "parent");

            _parent = parent;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return string.Format("{0} [{1}]", base.ToString(), Id);
        }

        /// <summary>
        /// Returns the hash code for this IDataObject.
        /// </summary>
        /// <returns>Returns the computed hash value.</returns>
        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }

        /// <summary>
        /// Gets the state (with all internal information and data)
        /// of the current data object. This procedure will store all
        /// data from the data object into the state.
        /// </summary>
        public IDataObjectState ExportState()
        {
            return new DataObjectStateLoader().LoadStateFrom(this);
        }

        /// <summary>
        /// Gets the state (with all internal information and data)
        /// of the current data object. This procedure will store only
        /// the fields given in the value names list into the state.
        /// </summary>
        /// <param name="valueNames">Specifies a list of value names which should be stored into the state.</param>
        public IDataObjectState ExportState(params string[] valueNames)
        {
            if (valueNames == null)
            {
                return ExportState();
            }
            return new DataObjectStateLoader().LoadStateFrom(this, valueNames);
        }

        /// <summary>
        /// Gets the state (with all internal information and data)
        /// of the current data object.
        /// </summary>
        /// <param name="toImport">Specifies the state to import.</param>
        /// <param name="beforeValueSet">Specifies the action which should be executed before a value is stored back to the data object.</param>
        /// <param name="afterValueSet">Specifies the action which should be executed after a value is stored back to the data object.</param>
        /// <returns>
        /// Returns the changes between the current and the imported state.
        /// </returns>
        public void ImportState(IDataObjectState toImport, Action<SchemaBase> beforeValueSet, Action<SchemaBase> afterValueSet)
        {
            PreCondition.AssertNotNull(toImport, "toImport");
            PreCondition.AssertNotNull(beforeValueSet, "beforeValueSet");
            PreCondition.AssertNotNull(afterValueSet, "afterValueSet");

            new DataObjectStateLoader().SaveStateTo(this, toImport, beforeValueSet, afterValueSet);
        }

        /// <summary>
        /// Gets the state (with all internal information and data)
        /// of the current data object.
        /// </summary>
        /// <param name="toImport">Specifies the state to import.</param>
        /// <returns>
        /// Returns the changes between the current and the imported state.
        /// </returns>
        public void ImportState(IDataObjectState toImport)
        {
            PreCondition.AssertNotNull(toImport, "toImport");
            new DataObjectStateLoader().SaveStateTo(this, toImport);
        }

        /// <summary>
        /// Sets the field with the given name. Fires an event before writing
        /// the value and after writing the value.
        /// </summary>
        /// <param name="name">Name of the field to write.</param>
        /// <param name="newValue">The new value to write. If the old value is the same to the new value it will not be overwritten.</param>
        /// <param name="beforeWriting">The action executed before writing the value. This argument may be null.</param>
        /// <param name="afterWriting">The action executed after writing the value. This argument may be null.</param>
        public void SetData(string name, object newValue, Action beforeWriting, Action afterWriting)
        {
            PreCondition.AssertNotNull(name, "name");

            object oldValue = this[name];

            if (oldValue == newValue
                || (newValue != null && newValue.Equals(oldValue))
                || (oldValue != null && oldValue.Equals(newValue)))
            {
                return;
            }

            if (beforeWriting != null)
                beforeWriting();

            this[name] = newValue;

            if (afterWriting != null)
                afterWriting();
        }

        /// <summary>
        /// Sets the field with the given name. Fires an event before writing
        /// the value and after writing the value. If the newValue is null,
        /// you will receive an argument null exception.
        /// </summary>
        /// <param name="name">Name of the field to write.</param>
        /// <param name="newValue">The new value to write. If the old value is the same to the new value it will not be overwritten.</param>
        /// <param name="beforeWriting">The action executed before writing the value. This argument may be null.</param>
        /// <param name="afterWriting">The action executed after writing the value. This argument may be null.</param>
        public void SetDataNotNull(string name, object newValue, Action beforeWriting, Action afterWriting)
        {
            PreCondition.AssertNotNull(name, "name");
            PreCondition.AssertNotNull(newValue, "newValue");

            SetData(name, newValue, beforeWriting, afterWriting);
        }

        /// <summary>
        /// Gets the data associated with the specified name.
        /// </summary>
        /// <typeparam name="T">Type of the returning value.</typeparam>
        /// <param name="name">The name of the field to retreive.</param>
        /// <returns>Returns either the retrieved value or a default value.</returns>
        public T GetData<T>(string name)
        {
            return GetData(name, default(T));
        }

        /// <summary>
        /// Gets the data associated with the specified name.
        /// </summary>
        /// <typeparam name="T">Type of the returning value.</typeparam>
        /// <param name="name">The name of the field to retreive.</param>
        /// <param name="defaultValue">Specifies the default value.</param>
        /// <returns>Returns either the retrieved value or a default value.</returns>
        public T GetData<T>(string name, T defaultValue)
        {
            PreCondition.AssertNotNull(name, "name");

            return (this[name] != null) ? (T)this[name] : defaultValue;
        }

        /// <summary>
        /// Gets the data associated with the specified name.
        /// </summary>
        /// <typeparam name="T">Type of the returning value.</typeparam>
        /// <param name="name">The name of the field to retreive.</param>
        /// <param name="converter">Converter which is used to convert a value into the expected type.</param>
        /// <returns>Returns either the retrieved value or a default value.</returns>
        public T GetData<T>(string name, Func<object, T> converter)
        {
            return GetData(name, converter, default(T));
        }

        /// <summary>
        /// Gets the data associated with the specified name.
        /// </summary>
        /// <typeparam name="T">Type of the returning value.</typeparam>
        /// <param name="name">The name of the field to retreive.</param>
        /// <param name="converter">Converter which is used to convert a value into the expected type.</param>
        /// <param name="defaultValue">Specifies the default value.</param>
        /// <returns>Returns either the retrieved value or a default value.</returns>
        public T GetData<T>(string name, Func<object, T> converter, T defaultValue)
        {
            PreCondition.AssertNotNull(name, "name");
            PreCondition.AssertNotNull(converter, "converter");

            return (this[name] != null) ? converter(this[name]) : defaultValue;
        }


        #region Value Check Methods

        /// <summary>
        /// Checks if the specified schema field is read only and throws an
        /// exception if a value should be set but this is not allowed.
        /// </summary>
        /// <param name="schemaField">Specifies the schema field definition with the underlying schema data.</param>
        protected virtual void CheckIsReadOnly(SchemaBase schemaField)
        {
            if (schemaField.IsSchemaElementReadOnly)
            {
                throw new InvalidOperationException(Resources.ExcDataObjArgSetNotSupported);
            }
        }

        /// <summary>
        /// Checks if the specified schema field has the corresponding value type and throws an
        /// exception if the value to set has an invalid type.
        /// </summary>
        /// <param name="schemaField">Specifies the schema field definition with the underlying schema data.</param>
        /// <param name="value">Specifies the value to check.</param>
        protected virtual void CheckValueType(SchemaBase schemaField, object value)
        {
            if (value != null && !schemaField.SchemaType.IsAssignableFrom(value.GetType()))
            {
                throw new ArgumentException(string.Format(Resources.ExcDataObjArgTypeInvalid, schemaField.SchemaName, schemaField.SchemaType.FullName), "value");
            }
        }

        /// <summary>
        /// Checks if the specified schema field has the corresponding allowed values type and throws an
        /// exception if the value to set is not contained in the schema definition.
        /// </summary>
        /// <param name="schemaField">Specifies the schema field definition with the underlying schema data.</param>
        /// <param name="value">Specifies the value to check.</param>
        protected virtual void CheckAllowedValues(SchemaBase schemaField, object value)
        {
            if (!schemaField.SchemaAllowedValues.IsEmpty() && !schemaField.SchemaAllowedValues.Contains(value))
            {
                throw new ArgumentException(string.Format(Resources.ExcDataObjArgValueInvalid, value, schemaField.SchemaName), "value");
            }
        }

        /// <summary>
        /// Checks if the specified value exceeds the corresponding max length.
        /// </summary>
        /// <param name="schemaField">Specifies the schema field definition with the underlying schema data.</param>
        /// <param name="value">Specifies the value to check.</param>
        protected virtual void CheckMaxLength(SchemaBase schemaField, object value)
        {
            if ((value is string) && schemaField.SchemaMaxLength > 0 && ((string)value).Length > schemaField.SchemaMaxLength)
            {
                throw new InvalidOperationException(Resources.ExcDataObjArgSetToLong);
            }
        }

        #endregion


        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// Specifies the value changed event which will be fired if an
        /// element value has changed on the underlying system. This event
        /// will not be fired if a value is changed via the SetData() method
        /// or via the indexer.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add { _pptyChangedEvents.Add(value); }
            remove { _pptyChangedEvents.Remove(value); }
        }

        /// <summary>
        /// Specifies the value changed event which will be fired before an
        /// element value has changed on the underlying system. This event
        /// will not be fired if a value is changed via the SetData() method
        /// or via the indexer.
        /// </summary>
        public event PropertyChangingEventHandler PropertyChanging
        {
            add { _pptyChangingEvents.Add(value); }
            remove { _pptyChangingEvents.Remove(value); }
        }

        /// <summary>
        /// Fires the PropertyChanged event.
        /// </summary>
        /// <param name="e">Specifies the event args to fire to the clients.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            _pptyChangedEvents.Raise(this, e);
        }

        /// <summary>
        /// Fires the PropertyChanging event.
        /// </summary>
        /// <param name="e">Specifies the event args to fire to the clients.</param>
        protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
        {
            _pptyChangingEvents.Raise(this, e);
        }

        /// <summary>
        /// Handles the PropertyChanged event which has been fired by an underlying
        /// data object.
        /// </summary>
        /// <param name="sender">Specifies the sender data object.</param>
        /// <param name="e">Specifies the event args which have been propagated to describe the event.</param>
        protected void OnDataPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // 1. search in schema mappings for an appropriate property
            if (NameSchemaMappings.ContainsDataObjectProperty(e.PropertyName))
            {
                OnPropertyChanged(new PropertyChangedEventArgs(NameSchemaMappings.GetSchema(e.PropertyName).SchemaName));
            }
            // 2. search in cache element to DAO mappings for an appropriate property
            else if (NamePropertyMappings.ContainsKey(e.PropertyName))
            {
                OnPropertyChanged(new PropertyChangedEventArgs(NamePropertyMappings[e.PropertyName]));
            }
        }

        /// <summary>
        /// Handles the PropertyChanging event which has been fired by an underlying
        /// data object.
        /// </summary>
        /// <param name="sender">Specifies the sender data object.</param>
        /// <param name="e">Specifies the event args which have been propagated to describe the event.</param>
        protected void OnDataPropertyChanging(object sender, PropertyChangingEventArgs e)
        {
            // 1. search in schema mappings for an appropriate property
            if (NameSchemaMappings.ContainsDataObjectProperty(e.PropertyName))
            {
                OnPropertyChanging(new PropertyChangingEventArgs(NameSchemaMappings.GetSchema(e.PropertyName).SchemaName));
            }
            // 2. search in cache element to DAO mappings for an appropriate property
            else if (NamePropertyMappings.ContainsKey(e.PropertyName))
            {
                OnPropertyChanging(new PropertyChangingEventArgs(NamePropertyMappings[e.PropertyName]));
            }
        }

        #endregion
    }
}